Every so often, there is a need to create a custom Window class. Typically, you do this via AfxRegisterWindowClass
, give the window a class name of your choosing, and then use this class in a Create
call. This class usually has a custom MFC subclass associated with it. The illustration to the left shows a little application with a custom control, a compass.
A typical example might be a desire to create a simple control with custom graphics. For this example, I created compass control, whose class will be CCompass
, and which will show a simulated compass needle. It is a subclass of "Generic CWnd".
To create this class, go into the ClassWizard, select the "Add Class" button, and select the option "New Class". Type in the name of your class, and in the "Base Class" box, select the option "generic CWnd", which appears nearly at the bottom of the options.
When you click OK, you will get two files, Compass.cpp and Compass.h, which implement your class.
When you are back in ClassWizard, this class should be selected as the class you want. For a custom graphics class, you will typically want to add a WM_ERASEBKGND
and WM_PAINT
handler. To do this, select the class in the window, select WM_ERASEBKGND
, click Add Function, select WM_PAINT
, and click Add Function. You should end up with something as shown below:
At this point, you can go in and fill in the two functions.
However, there is a problem with using this class in a dialog box. You must first register the "Window class" under a specific class name so the dialog editor can create it. This is necessary if you want to use the control in a CDialog
-derived class, CPropertyPage
-derived class, or CFormView
-derived class. This means you must provide a call to register the class, and this call must be executed before you attempt to create the class that contains the control.
This is inconvenient. Why should the programmer have to remember to do this; the consequence of not doing it is that the dialog does not come up.
I decided, in writing classes that my clients would want to use, that they should not be inconvenienced by having to remember to register the class, or understand the details of the AfxRegisterClass
call. So I decided to create a mechanism that would automatically register the class.
The technique was to create a static member variable of the class and initialize it. The initialization would, as a side effect, register the class. Because the variable is a static member variable, it will be initialized during application startup. Thus, the class would be automatically registered.
So I added the following declaration to the CCompass
class:
protected:
static BOOL hasclass;
static BOOL RegisterMe();
#define COMPASS_CLASS_NAME _T("Compass")
then in the CCompass.cpp file, I added:
BOOL CCompass::hasclass = CCompass::RegisterMe();
Note that because this is a static initializer, it will be executed at system startup. This means that the class registered by RegisterMe
will be registered when the application is initialized. The class will then be available for any dialogs, property pages, or form views.
However, some approaches will not work. For example, you cannot use AfxRegisterWndClass
because it returns the string for the synthesized class name, a name determined at execution time, but dialog templates require that you know the class name at the time the template is constructed. It would be the height of insanity to determine the string that AfxRegisterWndClass
returned and specify that as the class name the programmer should use.
In addition, you cannot call AfxGetInstanceHandle
to obtain the instance handle to register the class. This is because the variable used by AfxGetInstanceHandle
is initialized after the WinMain
of MFC is invoked, which is after the static member variables have been initialized. But you can use the low-level API call ::GetModuleHandle
. For compatibility with 16-bit Windows, this returns a type HMODULE
instead of HINSTANCE
, although this distinction has no meaning in Win32. However, you must do the explicit cast or the compiler becomes unhappy.
I also found that it works better if you choose ::DefWindowProc
as the window procedure instead of NULL
(this will be eventually replaced by AfxWndProc
when you subclass the window). Do not choose AfxWndProc
!
In the code below, I also made some arbitrary choices. For example, because this will be a child control, it does not need an icon, so the hIcon
member is set to NULL
. To illustrate how to choose a background brush, should you need one, I chose to use a standard background color, the dialog background, COLOR_BTNFACE
, and in accordance with the completely peculiar requirements of a window class (it would have made a great deal of sense, for example, to have not allowed the integer designator of 0 for a COLOR_color
, but this would have required careful design), I have to add 1 to the color. Since it is a child control it has no menu, and the lpszMenuName
is therefore NULL
. The critical parameter is the class name. This is the name the programmer must use in the dialog template.
BOOL CCompass::RegisterMe()
{
WNDCLASS wc;
wc.style = 0;
wc.lpfnWndProc = ::DefWindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = (HINSTANCE)::GetModuleHandle(NULL);
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = COMPASS_CLASS_NAME;
return AfxRegisterClass(&wc);
}
To put the control in a dialog, bring up the dialog editor. For step 1, select the "Custom Control" icon in the toolbox, the icon, and place the control in the desired section of the dialog box, as shown in step 2. Then bring up the Properties box. In step 3, delete the caption, and in step 4, type in the name of the class you used as COMPASS_CLASS_NAME
.
Unfortunately, ClassWizard is rather primitive; it will not acknowledge the existence of this control. Why? Ask Microsoft, I have no idea why it would preclude this control from its list of controls for which you can create a member variable. But it does.
So you have to edit your dialog "by hand". The Good News is that this is easy.
For example, locate in your dialog's header file the AFX_DATA
section. My dialog class is called CController
, and I have already used ClassWizard to create member variables for the range, speed, and altitude of the object being tracked.
enum { IDD = IDD_CONTROLLER };
CStatic c_Range;
CStatic c_Speed;
CStatic c_Altitude;
CCompass c_Compass;
What is interesting to note here is that once you add the variable, ClassWizard is perfectly happy to deal with it, it just won't let you add the variable! Weird!
Now go into the implementation file for your dialog and locate the DoDataExchange
method. Inside the AFX_DATA_MAP
section, add the line shown below. Note that it is identical in form to the other lines that create control variables, except that the control ID and variable name reflect the desired mapping. Again, once this is done, ClassWizard is happy to manage the control.
void CController::DoDataExchange(CDataExchange* pDX)
{
CFormView::DoDataExchange(pDX);
DDX_Control(pDX, IDC_RANGE, c_Range);
DDX_Control(pDX, IDC_SPEED, c_Speed);
DDX_Control(pDX, IDC_ALTITUDE, c_Altitude);
DDX_Control(pDX, IDC_COMPASS, c_Compass);
}
At this point, you are free to instantiate the dialog. Note that when your application loads, the class is registered, so even if you were to use a CFormView
in an SDI application, you need to take no further effort to use the class.
This control has some interesting properties from a GDI viewpoint. For example, I want a circular compass inside the control, but I would not like to constrain the designer of the dialog to choose a square dialog. I also don't want any annoying flashes within the compass as the background repaints.
To do this, I create a circular region that precludes the default WM_ERASEBKGND handler from touching the contents of the control. I then use this to limit the clip the output operations within the compass rose. This can also be used for hit-testing by using PtInRegion to see if the mouse is in the circular area.
The compass in its disabled and enabled modes is shown below.
CCompass::CreateClipRegion
CRect CCompass::CreateClipRegion(CRgn & rgn)
{
CRect r;
GetClientRect(&r);
int radius = min(r.Width() / 2, r.Height() / 2);
CPoint center(r.Width() / 2, r.Height() / 2);
rgn.CreateEllipticRgn(center.x - radius, center.y - radius,
center.x + radius, center.y + radius);
return CRect(center.x - radius, center.y - radius,
center.x + radius, center.y + radius);
}
CCompass::OnEraseBkgnd
BOOL CCompass::OnEraseBkgnd(CDC* pDC)
{
CRgn rgn;
CSaveDC sdc(pDC);
"#CreateClipRegion">CreateClipRegion(rgn);
pDC->SelectClipRgn(&rgn, RGN_DIFF);
return CWnd::OnEraseBkgnd(pDC);
}
CCompass::MapDC
Because of the frequency with which I map the DC, I created a separate method for this purpose.
void CCompass::MapDC(CDC & dc)
{
dc.SetMapMode(MM_ISOTROPIC);
CRect r;
GetClientRect(&r);
dc.SetWindowExt(r.Width(), r.Height());
dc.SetViewportExt(r.Width(), -r.Height());
CPoint center(r.left + r.Width() / 2, r.top + r.Height() / 2);
dc.SetViewportOrg(center.x, center.y);
}
CDoublePoint
This class lets me represent fractional angles. It turns out that other representations in the application were already using double precision, so it was a natural extension to use it in the compass. Note the simplistic CPoint
cast which truncates instead of rounding; this is sufficient for the application.
class CDoublePoint {
public:
CDoublePoint(){}
CDoublePoint(double ix, double iy) {x = ix; y = iy; }
double x;
double y;
operator CPoint() { CPoint pt; pt.x = (int)x; pt.y = (int)y; return pt; }
};
CCompass::OnLButtonDown
Button detection is done by responding only if the mouse is in the compass region. Note that I send a user-defined message to the parent, as described in mycompanion essay.
void CCompass::OnLButtonDown(UINT nFlags, CPoint point)
{
CRgn rgn;
"#CreateClipRegion">CreateClipRegion(rgn);
if(rgn.PtInRegion(point))
{
CClientDC dc(this);
"#MapDC">MapDC(dc);
dc.DPtoLP(&point);
GetParent()->SendMessage(CPM_CLICK, (WPARAM)point.x, (LPARAM)point.y);
return;
}
CWnd::OnLButtonDown(nFlags, point);
}
DegreesToRadians/GeographicToGeometric
I have a utility function that converts degrees to radians, declared in a separate header file.
__inline double DegreesToRadians(double x)
{ return (((x)/360.0) * (2.0 * 3.1415926535)); }
The normal geometric coordinate system has the angle 0.0 going to the right of the origin, and rotates counterclockwise with increasing angle. We want to think of degrees in the geographic sense, where 0.0 is North, 90.0 is East, 180.0 is South and 270.0 is West. The following inline method is useful for doing the conversion from the natural coordinates of geography to the coordinates required for the math.h library.
__inline double GeographicToGeometric(double x) { return -(x - 90.0); }
CCompass::CCompass
The constructor loads the table of coordinate designators.
CCompass::CCompass()
{
display.Add(new displayinfo( 0.0, _T("N"), 100.0, TRUE));
display.Add(new displayinfo( 90.0, _T("E"), 90.0, FALSE));
display.Add(new displayinfo(180.0, _T("S"), 90.0, FALSE));
display.Add(new displayinfo(270.0, _T("W"), 90.0, FALSE));
display.Add(new displayinfo( 45.0, _T("NE"), 80.0, FALSE));
display.Add(new displayinfo(135.0, _T("SE"), 80.0, FALSE));
display.Add(new displayinfo(225.0, _T("SW"), 80.0, FALSE));
display.Add(new displayinfo(315.0, _T("NW"), 80.0, FALSE));
RegistryString compass(IDS_COMPASS);
compass.load();
if(compass.value.GetLength() == 0 || !arrow.Read(compass.value))
arrow.Read(_T("Arrow.plt"));
angle = 0.0;
ArrowVisible = FALSE;
}
CCompass::OnPaint
void CCompass::OnPaint()
{
CPaintDC dc(this);
CBrush br(::GetSysColor(COLOR_INFOBK));
CRgn rgn;
CRect r;
r = "#CreateClipRegion">CreateClipRegion(rgn);
#define BORDER_WIDTH 2
CPen border(PS_SOLID, BORDER_WIDTH, RGB(0,0,0));
CBrush needle(RGB(255, 0, 0));
#define ENABLED_COLOR RGB(0,0,0)
#define DISABLED_COLOR RGB(128,128,128)
CPen enabledPen(PS_SOLID, 0, ENABLED_COLOR);
CPen disabledPen(PS_SOLID, 0, DISABLED_COLOR);
CSaveDC sdc(dc);
dc.SelectClipRgn(&rgn);
dc.FillRgn(&rgn, &br);
CPoint center(r.left + r.Width() / 2, r.top + r.Height() / 2);
r -= center;
int radius = r.Width() / 2;
dc.SetBkMode(TRANSPARENT);
"#MapDC">MapDC(dc);
{
CSaveDC sdc2(dc);
dc.SelectClipRgn(NULL);
dc.SelectStockObject(HOLLOW_BRUSH);
dc.SelectObject(&border);
dc.Ellipse(-radius, -radius, radius, radius);
r.InflateRect(-BORDER_WIDTH, -BORDER_WIDTH);
radius = r.Width() / 2;
}
radius = r.Width() / 2;
dc.SelectObject(IsWindowEnabled() ? &enabledPen : &disabledPen);
dc.MoveTo(0, radius);
dc.LineTo(0, -radius);
dc.MoveTo(-radius, 0);
dc.LineTo(radius, 0);
dc.MoveTo((int)(radius * sin("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric(225.0)))),
(int)(radius * cos("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric(225.0)))) );
dc.LineTo((int)(radius * sin("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric( 45.0)))),
(int)(radius * cos("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric( 45.0)))) );
dc.MoveTo((int)(radius * sin("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric(315.0)))),
(int)(radius * cos("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric(315.0)))) );
dc.LineTo((int)(radius * sin("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric(135.0)))),
(int)(radius * cos("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric(135.0)))) );
double size = 0.15 * (double)r.Width();
double CurrentFontSize = 0.0;
CFont * f = NULL;
dc.SetTextColor(IsWindowEnabled() ? ENABLED_COLOR : DISABLED_COLOR);
for(int i = 0; i < display.GetSize(); i++)
{
CSaveDC sdc2(dc);
dc.SetBkMode(OPAQUE);
dc.SetBkColor(::GetSysColor(COLOR_INFOBK));
if(display[i]->GetSize() != CurrentFontSize)
{
if(f != NULL)
delete f;
f = display[i]->CreateFont(size, _T("Times New Roman"));
}
dc.SelectObject(f);
CurrentFontSize = display[i]->GetSize();
CString text = display[i]->GetText();
int x = (int)(radius *
cos("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric(display[i]->GetAngle()))));
int y = (int)(radius *
sin("#DegreesToRadians">DegreesToRadians("#GeographicToGeometric">GeographicToGeometric(display[i]->GetAngle()))));
CSize textSize = dc.GetTextExtent(text);
double theta = display[i]->GetAngle();
if(theta == 0.0)
{
dc.SetTextAlign(TA_TOP | TA_LEFT);
x -= textSize.cx / 2;
}
else
if(theta < 90.0)
{
dc.SetTextAlign(TA_TOP | TA_RIGHT);
}
else
if(theta == 90.0)
{
dc.SetTextAlign(TA_TOP | TA_RIGHT);
y += textSize.cy / 2;
}
else
if(theta < 180.0)
{
dc.SetTextAlign(TA_BOTTOM | TA_RIGHT);
}
else
if(theta == 180.0)
{
dc.SetTextAlign(TA_BOTTOM | TA_LEFT);
x -= textSize.cx / 2;
}
else
if(theta < 270.0)
{
dc.SetTextAlign(TA_BOTTOM | TA_LEFT);
}
else
if(theta == 270)
{
dc.SetTextAlign(TA_TOP | TA_LEFT);
y += textSize.cy / 2;
}
else
{
dc.SetTextAlign(TA_TOP | TA_LEFT);
}
dc.TextOut(x, y, text);
}
if(f != NULL)
delete f;
if(IsWindowEnabled() && ArrowVisible)
{
CRect bb = arrow.GetInputBB();
dc.SelectObject(&needle);
arrow.Transform(angle, (double)abs(bb.Height()) / (2.0 * (double)radius));
arrow.Draw(dc, "#CDoublePoint">CDoublePoint(0.0, 0.0));
}
}